import def from './callbacks'

function Identity( v ) {
	return v;
}

function Thrower( ex ) {
	throw ex;
}

function slice( arr ) {
    return [].slice;
}

function adoptValue( value, resolve, reject, noValue ) {
	var method;

	try {

		// Check for promise aspect first to privilege synchronous behavior
		if ( value && def.isFunction( ( method = value.promise ) ) ) {
			method.call( value ).done( resolve ).fail( reject );

		// Other thenables
		} else if ( value && def.isFunction( ( method = value.then ) ) ) {
			method.call( value, resolve, reject );

		// Other non-thenables
		} else {

			// Control `resolve` arguments by letting Array#slice cast boolean `noValue` to integer:
			// * false: [ value ].slice( 0 ) => resolve( value )
			// * true: [ value ].slice( 1 ) => resolve()
			resolve.apply( undefined, [ value ].slice( noValue ) );
		}

	// For Promises/A+, convert exceptions into rejections
	// Since def.when doesn't unwrap thenables, we can skip the extra checks appearing in
	// Deferred#then to conditionally suppress rejection.
	} catch ( value ) {

		// Support: Android 4.0 only
		// Strict mode functions invoked without .call/.apply get global-object context
		reject.apply( undefined, [ value ] );
	}
}


def.Deferred = function(func ) {
	var tuples = [

			// action, add listener, callbacks,
			// ... .then handlers, argument index, [final state]
			[ "notify", "progress", def.Callbacks( "memory" ),
				def.Callbacks( "memory" ), 2 ],
			[ "resolve", "done", def.Callbacks( "once memory" ),
				def.Callbacks( "once memory" ), 0, "resolved" ],
			[ "reject", "fail", def.Callbacks( "once memory" ),
				def.Callbacks( "once memory" ), 1, "rejected" ]
		],
		state = "pending",
		promise = {
			state: function() {
				return state;
			},
			always: function() {
				deferred.done( arguments ).fail( arguments );
				return this;
			},
			"catch": function( fn ) {
				return promise.then( null, fn );
			},

			// Keep pipe for back-compat
			pipe: function( /* fnDone, fnFail, fnProgress */ ) {
				var fns = arguments;

				return def.Deferred( function(newDefer ) {
					def.each( tuples, function(i, tuple ) {

						// Map tuples (progress, done, fail) to arguments (done, fail, progress)
						var fn = def.isFunction( fns[ tuple[ 4 ] ] ) && fns[ tuple[ 4 ] ];

						// deferred.progress(function() { bind to newDefer or newDefer.notify })
						// deferred.done(function() { bind to newDefer or newDefer.resolve })
						// deferred.fail(function() { bind to newDefer or newDefer.reject })
						deferred[ tuple[ 1 ] ]( function() {
							var returned = fn && fn.apply( this, arguments );
							if ( returned && def.isFunction( returned.promise ) ) {
								returned.promise()
									.progress( newDefer.notify )
									.done( newDefer.resolve )
									.fail( newDefer.reject );
							} else {
								newDefer[ tuple[ 0 ] + "With" ](
									this,
									fn ? [ returned ] : arguments
								);
							}
						} );
					} );
					fns = null;
				} ).promise();
			},
			then: function( onFulfilled, onRejected, onProgress ) {
				var maxDepth = 0;
				function resolve( depth, deferred, handler, special ) {
					return function() {
						var that = this,
							args = arguments,
							mightThrow = function() {
								var returned, then;

								// Support: Promises/A+ section 2.3.3.3.3
								// https://promisesaplus.com/#point-59
								// Ignore double-resolution attempts
								if ( depth < maxDepth ) {
									return;
								}

								returned = handler.apply( that, args );

								// Support: Promises/A+ section 2.3.1
								// https://promisesaplus.com/#point-48
								if ( returned === deferred.promise() ) {
									throw new TypeError( "Thenable self-resolution" );
								}

								// Support: Promises/A+ sections 2.3.3.1, 3.5
								// https://promisesaplus.com/#point-54
								// https://promisesaplus.com/#point-75
								// Retrieve `then` only once
								then = returned &&

									// Support: Promises/A+ section 2.3.4
									// https://promisesaplus.com/#point-64
									// Only check objects and functions for thenability
									( typeof returned === "object" ||
										typeof returned === "function" ) &&
									returned.then;

								// Handle a returned thenable
								if ( def.isFunction( then ) ) {

									// Special processors (notify) just wait for resolution
									if ( special ) {
										then.call(
											returned,
											resolve( maxDepth, deferred, Identity, special ),
											resolve( maxDepth, deferred, Thrower, special )
										);

									// Normal processors (resolve) also hook into progress
									} else {

										// ...and disregard older resolution values
										maxDepth++;

										then.call(
											returned,
											resolve( maxDepth, deferred, Identity, special ),
											resolve( maxDepth, deferred, Thrower, special ),
											resolve( maxDepth, deferred, Identity,
												deferred.notifyWith )
										);
									}

								// Handle all other returned values
								} else {

									// Only substitute handlers pass on context
									// and multiple values (non-spec behavior)
									if ( handler !== Identity ) {
										that = undefined;
										args = [ returned ];
									}

									// Process the value(s)
									// Default process is resolve
									( special || deferred.resolveWith )( that, args );
								}
							},

							// Only normal processors (resolve) catch and reject exceptions
							process = special ?
								mightThrow :
								function() {
									try {
										mightThrow();
									} catch ( e ) {

										if ( def.Deferred.exceptionHook ) {
											def.Deferred.exceptionHook( e,
												process.stackTrace );
										}

										// Support: Promises/A+ section 2.3.3.3.4.1
										// https://promisesaplus.com/#point-61
										// Ignore post-resolution exceptions
										if ( depth + 1 >= maxDepth ) {

											// Only substitute handlers pass on context
											// and multiple values (non-spec behavior)
											if ( handler !== Thrower ) {
												that = undefined;
												args = [ e ];
											}

											deferred.rejectWith( that, args );
										}
									}
								};

						// Support: Promises/A+ section 2.3.3.3.1
						// https://promisesaplus.com/#point-57
						// Re-resolve promises immediately to dodge false rejection from
						// subsequent errors
						if ( depth ) {
							process();
						} else {

							// Call an optional hook to record the stack, in case of exception
							// since it's otherwise lost when execution goes async
							if ( def.Deferred.getStackHook ) {
								process.stackTrace = def.Deferred.getStackHook();
							}
							window.setTimeout( process );
						}
					};
				}

				return def.Deferred( function(newDefer ) {

					// progress_handlers.add( ... )
					tuples[ 0 ][ 3 ].add(
						resolve(
							0,
							newDefer,
                            def.isFunction( onProgress ) ?
								onProgress :
								Identity,
							newDefer.notifyWith
						)
					);

					// fulfilled_handlers.add( ... )
					tuples[ 1 ][ 3 ].add(
						resolve(
							0,
							newDefer,
                            def.isFunction( onFulfilled ) ?
								onFulfilled :
								Identity
						)
					);

					// rejected_handlers.add( ... )
					tuples[ 2 ][ 3 ].add(
						resolve(
							0,
							newDefer,
                            def.isFunction( onRejected ) ?
								onRejected :
								Thrower
						)
					);
				} ).promise();
			},

			// Get a promise for this deferred
			// If obj is provided, the promise aspect is added to the object
			promise: function( obj ) {
                return obj != null ? def.extend( obj, promise ) : promise;
			}
		},
		deferred = {};

	// Add list-specific methods
	def.each( tuples, function(i, tuple ) {
		var list = tuple[ 2 ],
			stateString = tuple[ 5 ];

		// promise.progress = list.add
		// promise.done = list.add
		// promise.fail = list.add
		promise[ tuple[ 1 ] ] = list.add;

		// Handle state
		if ( stateString ) {
			list.add(
				function() {

					// state = "resolved" (i.e., fulfilled)
					// state = "rejected"
					state = stateString;
				},

				// rejected_callbacks.disable
				// fulfilled_callbacks.disable
				tuples[ 3 - i ][ 2 ].disable,

				// rejected_handlers.disable
				// fulfilled_handlers.disable
				tuples[ 3 - i ][ 3 ].disable,

				// progress_callbacks.lock
				tuples[ 0 ][ 2 ].lock,

				// progress_handlers.lock
				tuples[ 0 ][ 3 ].lock
			);
		}

		// progress_handlers.fire
		// fulfilled_handlers.fire
		// rejected_handlers.fire
		list.add( tuple[ 3 ].fire );

		// deferred.notify = function() { deferred.notifyWith(...) }
		// deferred.resolve = function() { deferred.resolveWith(...) }
		// deferred.reject = function() { deferred.rejectWith(...) }
		deferred[ tuple[ 0 ] ] = function() {
			deferred[ tuple[ 0 ] + "With" ]( this === deferred ? undefined : this, arguments );
			return this;
		};

		// deferred.notifyWith = list.fireWith
		// deferred.resolveWith = list.fireWith
		// deferred.rejectWith = list.fireWith
		deferred[ tuple[ 0 ] + "With" ] = list.fireWith;
	} );

	// Make the deferred a promise
	promise.promise( deferred );

	// Call given func if any
	if ( func ) {
		func.call( deferred, deferred );
	}

	// All done!
	return deferred;
};

// Deferred helper
def.when = function(singleValue ) {
	var

		// count of uncompleted subordinates
		remaining = arguments.length,

		// count of unprocessed arguments
		i = remaining,

		// subordinate fulfillment data
		resolveContexts = Array( i ),
		resolveValues = slice.call( arguments ),

		// the master Deferred
		master = def.Deferred(),

		// subordinate callback factory
		updateFunc = function( i ) {
			return function( value ) {
				resolveContexts[ i ] = this;
				resolveValues[ i ] = arguments.length > 1 ? slice.call( arguments ) : value;
				if ( !( --remaining ) ) {
					master.resolveWith( resolveContexts, resolveValues );
				}
			};
		};

	// Single- and empty arguments are adopted like Promise.resolve
	if ( remaining <= 1 ) {
		adoptValue( singleValue, master.done( updateFunc( i ) ).resolve, master.reject,
			!remaining );

		// Use .then() to unwrap secondary thenables (cf. gh-3000)
		if ( master.state() === "pending" ||
			def.isFunction( resolveValues[ i ] && resolveValues[ i ].then ) ) {

			return master.then();
		}
	}

	// Multiple arguments are aggregated like Promise.all array elements
	while ( i-- ) {
		adoptValue( resolveValues[ i ], updateFunc( i ), master.reject );
	}

	return master.promise();
};

export  default def;